--[[
[01][02][03][04]|[17][21][25][29]
[12][13][14][05]|[18][22][26]....
[11][16][15][06]|[19][23][27]
[10][09][08][07]|[20][24][28]

Icon behaviors:
    spells:
        'default' or 'cooldown' or nil -- show cooldown timer for spell, always visible
        'reminder' -- show when spell off cooldown, hide when spell is on cooldown

    auras:
        'default' or nil -- show when active, hide when inactive
        'indicator'   -- show when aura active, shade when inactive
        'reminder'    -- show when aura absent, hide when aura present
]]

--
-- Local references to globals
--
local Rotor = _G.Rotor

local CreateFrame = _G.CreateFrame
local UIParent = _G.UIParent
local GetTime = _G.GetTime
local max = _G.max
local format = _G.format
local IsSpellKnown = _G.IsSpellKnown

--
-- Icons positioning and sizing
--

--
-- Offsets of icons top left corner from main spell bottom left icon's corner
--
local icon_offsets

local SPACING = 1 -- spacing between icons

local SPELL_BUFF_BORDER_COLOR = {0, 0.8, 0}

local last_main_icon_size

local function calcIconSizesAndOffsets()
    if last_main_icon_size == Rotor.cfg.main_icon_size then
        return -- already calculated
    end

    local spacing = Rotor.cfg.icon_spacing or SPACING

    -- button size changed, recalculate
    Rotor.icon_size = math.floor(Rotor.cfg.main_icon_size / 2)
    local out_width = Rotor.icon_size * 4 + (spacing * 3)
    Rotor.main_icon_size = out_width - (Rotor.icon_size * 2 + spacing * 2)

    icon_offsets = {}

    -- current offset. set to tl corner of icon1
    local xoff = -spacing - Rotor.icon_size
    local yoff = Rotor.main_icon_size + spacing + Rotor.icon_size

    local function cur()
        return {xoff, yoff}
    end

    local function right()
        xoff = xoff + spacing + Rotor.icon_size
        return cur()
    end

    local function down()
        yoff = yoff - spacing - Rotor.icon_size
        return cur()
    end

    local function left()
        xoff = xoff - spacing - Rotor.icon_size
        return cur()
    end

    local function up()
        yoff = yoff + spacing + Rotor.icon_size
        return cur()
    end

    local calc_functions = {
        cur,    -- 01
        right,  -- 02
        right,  -- 03
        right,  -- 04
        down,   -- 05
        down,   -- 06
        down,   -- 07
        left,   -- 08
        left,   -- 09
        left,   -- 10
        up,     -- 11
        up,     -- 12
        right,  -- 13
        right,  -- 14
        down,   -- 15
        left,   -- 16
    }

    for i = 1, 16 do
        icon_offsets[i] = calc_functions[i]()
    end

    last_main_icon_size = Rotor.cfg.main_icon_size
end
----------------------------------------
local function make_int_color(r_prop, g_prop, b_prop, a_prop)
    local value = math.floor(r_prop * 255) * 65536 + math.floor(g_prop * 255) * 256 + math.floor(b_prop * 255)
    if a_prop then
        value = value + math.floor(a_prop * 255) * 16777216
    end
    return value
end

--
-- Icon class definition
--

local RotorIcon = {
    visible = true,
    cooldown_visible = true,
    last_cooldown_end_millis = 0,
    texture_name = '',
    vertex_int_color = make_int_color(1, 1, 1),
    icon_type = '',
    last_displayed = nil,
    update = nil,
    position = -1,
    last_int_bordercolor = -1,
}

local icons_by_postions = {}

--
-- Icon constructor function
--
function Rotor:createIcon(name, pos)
    calcIconSizesAndOffsets()

    local icon = nil

    if (pos or -1) == -1 then
        icon = RotorIcon:new(name, pos or -1) -- main icon
    else
        icon = icons_by_postions[pos]
        if icon then
            icon:awake()
        else
            icon = RotorIcon:new(name, pos)
            icons_by_postions[pos] = icon
        end
    end

    return icon
end
----------------------------------------
function Rotor:getIconAtPos(icon_pos)
    if not icon_pos then
        -- allocate icon at free position after 17-th
        icon_pos = 17
        while self.icons[icon_pos] do
            icon_pos = icon_pos + 1
        end
    end

    local icon = self.icons[icon_pos]
    if not icon then
        icon = self:createIcon('RotorIcon'..tonumber(icon_pos), icon_pos)
        self.icons[icon_pos] = icon
    end

    return icon
end
----------------------------------------
function Rotor:initIcons()
    calcIconSizesAndOffsets()

    for _, icon in pairs(self.icons) do
        icon:release()
    end

    self.icons = {}
end
----------------------------------------
function Rotor:updateIcons()
    for _, icon in pairs(self.icons) do
        icon:update()
    end
end

----------------------------------------
--
-- RotorIcon
--
function RotorIcon:new(name, pos)
    local parent

    if pos and ((pos == 0) or (pos >= 13 and pos <= 16)) then
        -- icons on main icon
        parent = Rotor.main_icon_overlay
    else
        parent = UIParent
    end
    icon = CreateFrame('Button', name, parent)
    for k, v in pairs(RotorIcon) do
        if k[1] ~= '_' then
            icon[k] = v
        end
    end

    icon.name = name
    icon:init(pos)
    return icon
end
----------------------------------------
function RotorIcon:setupVisualParameters()
    local size

    if self.position == -1 then
        -- main icon
        size = Rotor.main_icon_size
    else
        size = Rotor.icon_size
    end

    self:SetWidth(size)
    self:SetHeight(size)
    self:SetScale(Rotor.cfg.framescale)
    self:SetAlpha(Rotor.cfg.framealpha)

    if self.position == 0 then
        -- small center icon
        self:ClearAllPoints()
        self:SetPoint('CENTER', Rotor.main_icon, 'CENTER', 0, 0)
    elseif self.position ~= -1 then
        local offsets = icon_offsets[self.position]
        if not offsets then
            error('no offsets')
        end
        self:ClearAllPoints()
        self:SetPoint('TOPLEFT', Rotor.main_icon, 'BOTTOMLEFT', offsets[1], offsets[2])
    end
end
----------------------------------------
function RotorIcon:init(pos)
    self:Hide()
    self:EnableMouse(false)

    self.position = pos or -1

    calcIconSizesAndOffsets()

    if self.position == -1 then
        -- disable digital timers for main spell
        self.noCooldownCount = true
        self.noOOC = true
    end

    self:SetBackdrop({
        bgFile = 'Interface\\Buttons\\WHITE8x8',
        edgeFile = 'Interface\\Buttons\\WHITE8x8',
        tile = false, tileSize = 0, edgeSize = 1,
        insets = {left = -1, right = -1, top = -1, bottom = -1}
    })
    self:SetBackdropColor(unpack(Rotor.cfg.backdropcolor))
    self:setBorderColor(Rotor.cfg.bordercolor)

    -- spell texture
    local texture = self:CreateTexture(self.name .. '_texture', 'PARENT')
    texture:SetVertexColor(1.0, 1.0, 1.0)
    texture:SetTexCoord(0.1, 0.9, 0.1, 0.9)
    texture:ClearAllPoints()
    texture:SetPoint('TOPLEFT', self, 'TOPLEFT', 2, -2)
    texture:SetPoint('BOTTOMRIGHT', self, 'BOTTOMRIGHT', -2, 2)
    texture:SetTexture(Rotor.default_texture)
    self.texture = texture

    -- cooldown spiral
    local cd = CreateFrame('Cooldown', self.name .. '_cd', self)
    cd:ClearAllPoints()
    cd:SetPoint('TOPLEFT', self, 'TOPLEFT', 1, -1)
    cd:SetPoint('BOTTOMRIGHT', self, 'BOTTOMRIGHT', -1, 1)
    self.cd = cd

    self:setupVisualParameters()

    self:hide()

    return self
end
----------------------------------------
function RotorIcon:awake()
    self:setupVisualParameters()
end
----------------------------------------
function RotorIcon:hideCooldown()
    if self.cooldown_visible then
        self.cd:Hide()
        self.cooldown_visible = false
    end
end
----------------------------------------
function RotorIcon:hide()
    if self.visible then
        self:Hide()
        self.visible = false
    end
end
----------------------------------------
function RotorIcon:show()
    if not self.visible then
        self:Show()
        self.visible = true
    end
end
----------------------------------------
function RotorIcon:setCooldown(start, duration)
    local cd_end_millis = math.floor((start + duration) * 1000.0)
    local last_cd_end_millis = self.last_cooldown_end_millis
    local now_millis = math.floor(GetTime() * 1000.0)

    if cd_end_millis <= now_millis then
        cd_end_millis, start, duration = 0, 0, 0
    end

    if last_cd_end_millis <= now_millis then
        last_cd_end_millis = 0
    end

    if cd_end_millis ~= last_cd_end_millis then
        self.last_cooldown_end_millis = cd_end_millis
        self.cd:SetCooldown(start, duration)
        if not self.cooldown_visible then
            self.cd:Show()
            self.cooldown_visible = true
        end
    end
end
----------------------------------------
function RotorIcon:setTexture(texture_name)
    if self.texture_name ~= texture_name then
        self.texture:SetTexture(texture_name)
        self.texture_name = texture_name
    end
end
----------------------------------------
function RotorIcon:setVertexColor(r_prop, g_prop, b_prop)
    local vertex_int_color = make_int_color(r_prop, g_prop, b_prop)

    if vertex_int_color ~= self.vertex_int_color then
        self.texture:SetVertexColor(r_prop, g_prop, b_prop)
        self.vertex_int_color = vertex_int_color
    end
end
----------------------------------------
function RotorIcon:shade(color)
    local r, g, b = unpack(color or Rotor.cfg.icon_shade_color)
    self:setVertexColor(r, g, b)
end
----------------------------------------
function RotorIcon:unshade()
    self:setVertexColor(1, 1, 1)
end
----------------------------------------
function RotorIcon:shadeOOR()
    self:shade(Rotor.cfg.oor_icon_shade_color)
end
----------------------------------------
function RotorIcon:dummyUpdate()
    -- do nothing
end
----------------------------------------
function RotorIcon:release()
    self:hide()
    self:unshade()
    self:setDefaultBorderColor()
    self.obj = nil
    self.icon_type = ''
    self.last_displayed = nil
    self.last_cooldown_end_millis = 0
    self:setCooldown(0, 0)
    self.texture_name = ''
    self.check_enabled = nil
end
----------------------------------------
function RotorIcon:setBorderColor(color)
    local r, g, b, a = unpack(color)
    local int_color = make_int_color(r, g, b, a)
    if self.last_int_bordercolor ~= int_color then
        self:SetBackdropBorderColor(r, g, b, a)
        self.last_int_bordercolor = int_color
    end
end
----------------------------------------
function RotorIcon:setDefaultBorderColor()
    self:setBorderColor(Rotor.cfg.bordercolor)
end

--
-- Aura section
--
function RotorIcon:addAura(aura_rec, behavior)
    if self.icon_type == 'spell' then
        error('icon is already a spell')
        return
    end
    self.icon_type = 'aura'

    if not self.obj then
        if behavior == 'indicator' then
            self.obj = aura_rec
        else
            self.obj = {aura_rec}
        end
    else
        if behavior == 'indicator' then
            error('aura indicator icon must have one aura')
            return
        end
        table.insert(self.obj, aura_rec)
    end

    local upd_func = self.dummyUpdate

    if (behavior or 'default') == 'default' then
        upd_func = self.updateAuraDefault
    elseif behavior == 'indicator' then
        upd_func = self.updateAuraIndicator
        self.setTexture(aura_rec.texture_name)
    elseif behavior == 'reminder' then
        upd_func = self.updateAuraReminder
        self:hideCooldown()
    else
        error('unknown aura icon behavior: '..tostring(behavior))
    end

    if not self.update then
        self.update = upd_func
    elseif self.update ~= upd_func then
        error('can not have more than one behavior for one icon')
    end
end
----------------------------------------
function RotorIcon:updateAuraDefault()
    local most_active

    for _, aura in pairs(self.obj) do
        if aura.active then
            if not most_active or (aura.time_left > most_active.time_left) then
                most_active = aura
            end
        end
    end

    if not most_active then
        self.last_displayed = nil
        self:hide()
    else
        if self.last_displayed ~= most_active then
            self.last_displayed = most_active
            self.texture:SetTexture(most_active.texture_name)
        end
        if most_active.duration ~= 0 then
            self:setCooldown(most_active.ends - most_active.duration, most_active.duration)
        else
            self:hideCooldown()
        end
        self:show()
    end
end
----------------------------------------
function RotorIcon:updateAuraIndicator()
    if self.obj.active then
        if self.obj.duration ~= 0 then
            self:setCooldown(self.obj.ends - self.obj.duration, self.obj.duration)
        else
            self:hideCooldown()
        end
        self:unshade()
    else
        self:hideCooldown()
        self:shade()
    end
end
----------------------------------------
function RotorIcon:updateAuraReminder()
    local first_absent

    for i, k in pairs(self.obj) do
        if not k.active then
            first_absent = k
            break
        end
    end

    if not first_absent then
        self.last_displayed = nil
        self:hide()
    else
        if self.last_displayed ~= first_absent then
            self.last_displayed = first_absent
            self.texture:SetTexture(first_absent.texture_name)
        end
        self:show()
    end
end

--
-- Spell functions
--
function RotorIcon:initForSpell(spell_rec, behavior)
    if self.icon_type == 'aura' then
        error('icon is already an aura')
    elseif self.icon_type == 'spell' then
        error('icon already contains spell')
    end
    self.icon_type = 'spell'

    self.obj = spell_rec
    self:setTexture(spell_rec.texture_name)

    if ((behavior or 'default') == 'default') or (behavior == 'cooldown') then
        self.update = self.updateSpellDefault
    elseif behavior == 'reminder' then
        self.update = self.updateSpellReminder
        self:hideCooldown()
    else
        error('unknown spell behavior: '..tostring(behavior))
    end
end
----------------------------------------
function RotorIcon:isSpellUsable(spell)
    if spell.customCheckUsable then
        return spell:customCheckUsable() -- complete custom check
    else
        return spell.usable
            and ((spell.off_gcd and spell.cd < 0.00001) or (spell.cd - max(Rotor.end_of_gcd - GetTime(), 0) <= 0.1))
            and (not spell.min_power or (spell.min_power <= Rotor.power))
            and (not spell.check_enabled or spell.check_enabled())
            and (not spell.min_combo_points or (spell.min_combo_points <= Rotor.combo_points))
            and (not self.check_enabled or self.check_enabled())
    end
end
----------------------------------------
function RotorIcon:updateSpellDefault()
    local spell = self.obj
    local aura = spell.aura

    if spell.is_pet_spell and (not IsSpellKnown(spell.id, true) or not Rotor.pet_alive) then
        self:hide()
        return
    end

    if aura and aura.active then
        if aura.duration ~= 0 then
            self:setCooldown(aura.ends - aura.duration, aura.duration)
        end
        self:setBorderColor(SPELL_BUFF_BORDER_COLOR)
        self:unshade()
    else
        self:setDefaultBorderColor()
        if (spell.cd > 0) and (spell.cooldown_duration > 1.5) then
            self:setCooldown(spell.cooldown_start, spell.cooldown_duration)
            self:unshade()
        else
            self:setCooldown(0, 0)
            if self:isSpellUsable(spell) then
                if not spell.range_check or Rotor:CheckInRange(spell.id) then
                    self:unshade()
                else
                    self:shadeOOR()
                end
            else
                self:shade()
            end
        end
    end
    self:show()
end
----------------------------------------
function RotorIcon:updateSpellReminder()
    local spell = self.obj
    local aura = spell.aura

    if spell.is_pet_spell and (not IsSpellKnown(spell.id, true) or not Rotor.pet_alive) then
        self:hide()
        return
    end

    if aura and aura.active then
        if aura.duration ~= 0 then
            self:setCooldown(aura.ends - aura.duration, aura.duration)
        else
            self:setCooldown(0, 0)
        end
        self:unshade()
        self:setBorderColor(SPELL_BUFF_BORDER_COLOR)
        self:show()
    else
        self:hideCooldown()
        self:setDefaultBorderColor()
        if self:isSpellUsable(spell) then
            if (not spell.range_check) or Rotor:CheckInRange(spell.id) then
                self:unshade()
            else
                self:shadeOOR()
            end
            self:show()
        else
            self:hide()
        end
    end
end
